﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using YAXLib;
using RaytracerLib.MathLib;
using System.Diagnostics;
using RaytracerLib.Rays;
using RaytracerLib.MathLib.Polynomials;
using RaytracerLib.Materials;
using RaytracerLib.UVMappers;

namespace RaytracerLib.RenderableObjects
{
    /// <summary>
    /// Cone pin-point lays in center of local coords system. Base lays in Z plane at y=H. Pos point to base disc 
    /// position. Cone in local coords space has H=1 and R=1. In worlds coords Pos points to center of cone. 
    /// Up vector is directred from Pos to pin-point.
    /// </summary>
    public class ConeObject : RenderableObject
    {
        [YAXNode("Radius")]
        private double m_radius;

        [YAXNode("Height")]
        private double m_height;

        public ConeObject(Vector3 a_right, Vector3 a_up) :
            base(a_right, a_up)
        {
            Name = "Cone";
            Closed = true;
            m_uv_mapper = new CylindricalUVMapper();
            Update(UpdateFlags.All);
        }

        public Material BaseMaterial
        {
            get
            {
                return Materials[1];
            }
            set
            {
                Materials[1] = value;
            }
        }

        public double Radius
        {
            get
            {
                return m_radius;
            }
            set
            {
                m_radius = value;
                Update(UpdateFlags.BoundBox | UpdateFlags.Matrices | UpdateFlags.UVMapper);
            }
        }

        public double Height
        {
            get
            {
                return m_height;
            }
            set
            {
                m_height = value;
                Update(UpdateFlags.BoundBox | UpdateFlags.Matrices | UpdateFlags.UVMapper);
            }
        }

        protected override Matrix4 GetLocalToWorldMatrix()
        {
            return Matrix4.CreateTranslation(Pos) *
                    new Matrix4(Right, Up, Forward) *
                    Matrix4.CreateTranslation(0, Height / 2, 0) * 
                    Matrix4.CreateRotateX(180) * 
                    Matrix4.CreateScale(Scale.Scale(LocalScale));
        }

        internal override Intersection GetIntersection(Intersection a_source_ray_intersection, Ray a_ray)
        {
            bool back_hit_expected = false;

            if ((a_source_ray_intersection != null) && a_source_ray_intersection.SceneObject == this)
            {
                Debug.Assert((a_ray.RaySurfaceSide == RaySurfaceSide.RefractedSide) ||
                                (a_ray.RaySurfaceSide == RaySurfaceSide.ReflectedSide));
                if (a_source_ray_intersection.BackHit ^ (a_ray.RaySurfaceSide == RaySurfaceSide.ReflectedSide))
                    return Scene.NoIntersection;
                else
                {
                    if (OneSide)
                        return Scene.NoIntersection;
                    else
                        back_hit_expected = true;
                }
            }

            Vector3 local_dir;
            Vector3 local_start;

            if (!TransformToLocalToBoundBox(a_ray, out local_dir, out local_start))
                return Scene.NoIntersection;

            double dist = Double.PositiveInfinity;

            PolynomialSolverAlgebraic solver = new PolynomialSolverAlgebraic(
                new Polynomial(
                    local_start.X * local_start.X + local_start.Z * local_start.Z - local_start.Y * local_start.Y, 
                    2 * (local_dir.X * local_start.X + local_dir.Z * local_start.Z - local_dir.Y * local_start.Y),
                    1 - 2 * local_dir.Y * local_dir.Y));

            double dist1 = default(Double), dist2 = default(Double);
            int roots = solver.SolveAlgebraicQuadratic(ref dist1, ref dist2);

            if (roots == 0)
                return Scene.NoIntersection;

            if ((roots >= 1) && (dist1 > Constants.MINIMAL_DISTANT))
            {
                double ly = local_start.Y + local_dir.Y * dist1;

                if ((ly >= Constants.MINIMAL_DISTANT) && (ly < 1))
                    dist = dist1;
            }

            if ((roots == 2) && (dist2 > Constants.MINIMAL_DISTANT) && (dist2 < dist))
            {
                double ly = local_start.Y + local_dir.Y * dist2;

                if ((ly >= Constants.MINIMAL_DISTANT) && (ly < 1))
                    dist = dist2;
            }

            double base_dist = (1 - local_start.Y) / local_dir.Y;
            bool base_hit = false;

            if ((base_dist > Constants.MINIMAL_DISTANT) && (base_dist < dist))
            {
                double lx = local_start.X + local_dir.X * base_dist;
                double lz = local_start.Z + local_dir.Z * base_dist;

                if (lx * lx + lz * lz < 1)
                {
                    base_hit = true;
                    dist = base_dist;
                }
            }

            if (dist == Double.PositiveInfinity)
                return Scene.NoIntersection;

            bool back_hit = false;

            if (back_hit_expected)
                back_hit = true;
            else
            {
                if (base_hit)
                    back_hit = local_start.Y < 1;
                else
                {
                    if ((local_start.Y > 0) && (local_start.Y < 1))
                    {
                        back_hit = local_start.X * local_start.X + local_start.Z * local_start.Z <
                            local_start.Y * local_start.Y;
                    }
                }
            }

            if (back_hit && OneSide)
                return Scene.NoIntersection;

            Vector3 local_pos = local_start + local_dir * dist;
            Vector3 world_pos = LocalToWorld * local_pos;
            dist = (world_pos - a_ray.Start).Length;

            return new Intersection()
            {
                PrevIntersection = a_source_ray_intersection,
                SceneObject = this,
                SourceRay = a_ray,
                Dist = dist,
                Scene = Scene,
                BackHit = back_hit,
                Pos = world_pos,
                LocalPos = local_pos,
                Material = base_hit ? BaseMaterial : DefaultMaterial
            };
        }

        internal override Vector3 GetNormal(Intersection a_intersection)
        {
            if (a_intersection.LocalPos.Y.IsAlmostEquals(1))
            {
                if (a_intersection.BackHit)
                    return Up;
                else
                    return -Up;
            }
            else
            {
                if (a_intersection.BackHit)
                {
                    return (LocalToWorldNormal * new Vector3(-a_intersection.LocalPos.X,
                        a_intersection.LocalPos.Y, -a_intersection.LocalPos.Z)).Normalized;
                }
                else
                {
                    return (LocalToWorldNormal * new Vector3(a_intersection.LocalPos.X,
                        -a_intersection.LocalPos.Y, a_intersection.LocalPos.Z)).Normalized;
                }
            }
        }

        public override string ToString()
        {
            return String.Format("Cone: {0}", Name);
        }

        internal override Vector3 GetUVW(Intersection a_intersection)
        {
            Vector3 uvw = base.GetUVW(a_intersection);

            return new Vector3(uvw.X / 2 + 0.5, uvw.Y, -uvw.Z / 2 + 0.5);
        }

        internal override void GetTangents(Intersection a_intersection,
            out Vector3 a_tangent_x, out Vector3 a_tangent_y)
        {
            if (a_intersection.LocalPos.Y.IsAlmostEquals(1))
            {
                a_tangent_x = Right;
                a_tangent_y = Forward;
            }
            else
            {
                if (a_intersection.BackHit)
                {
                    a_tangent_x = Vector3.CrossProduct(a_intersection.Normal, Up).Normalized;
                    a_tangent_y = Vector3.CrossProduct(a_intersection.Normal, a_tangent_x).Normalized;
                }
                else
                {
                    a_tangent_x = Vector3.CrossProduct(Up, a_intersection.Normal).Normalized;
                    a_tangent_y = Vector3.CrossProduct(a_tangent_x, a_intersection.Normal).Normalized;
                }
            }
        }

        protected override Vector3 LocalScale
        {
            get
            {
                return new Vector3(Radius, Height, Radius);
            }
        }

        protected override AABB GetLocalBoundBox()
        {
            return new AABB(new Vector3(-1, 0, -1), new Vector3(1, 1, 1));
        }

        public override void ScaleAbsolute(double a_scale)
        {
            Radius *= a_scale;
            Height *= a_scale;

            base.ScaleAbsolute(a_scale);
        }

        protected override void Update(UpdateFlags a_flags)
        {
            base.Update(a_flags);

            if (a_flags.HasFlag(UpdateFlags.UVMapper))
            {
                if (m_uv_mapper is ConicalUVMapper)
                {
                    (m_uv_mapper as ConicalUVMapper).Height = Height;
                    (m_uv_mapper as ConicalUVMapper).Radius = Radius;
                }
            }
        }
    }
}
